fix: match installed skills by source repo, not just name#205
fix: match installed skills by source repo, not just name#205mwmdev wants to merge 2 commits intospacedriveapp:mainfrom
Conversation
WalkthroughAdds optional source_repo to skill metadata across backend, API, core model, installer, and frontend. Installer injects provenance into SKILL.md for GitHub installs. Frontend uses composite keys (source_repo/name) to disambiguate installed skills. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/skills/installer.rs (1)
36-41:⚠️ Potential issue | 🟠 MajorNo timeout on the GitHub archive download.
reqwest::Client::new()uses no timeout; a stalled or slow response from GitHub will hang the install task indefinitely. The registry proxy insrc/api/skills.rsalready uses.timeout(Duration::from_secs(10))as a precedent.🔧 Proposed fix
- let client = reqwest::Client::new(); - let response = client - .get(&download_url) - .send() + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(60)) + .build() + .context("failed to build HTTP client")?; + let response = client + .get(&download_url) + .send()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/skills/installer.rs` around lines 36 - 41, The download currently uses reqwest::Client::new() with no timeout which can hang; replace the client creation with a timed client (use reqwest::Client::builder().timeout(Duration::from_secs(10)).build()) and use that client for the existing client.get(&download_url).send().await call, adding a std::time::Duration import if missing so the GitHub archive download in installer.rs times out like the registry proxy does.
🧹 Nitpick comments (2)
interface/src/routes/AgentSkills.tsx (2)
234-240: Core composite-key logic is correct; document the known limitation of the name-only fallback.The key shapes match correctly:
- New installs:
installedKeysentry ="${source_repo}/${name}"(e.g."anthropics/skills/weather");compositeKey="${skill.source}/${skill.name}"→ same shape → correct match.- Different source, same name: keys differ → no false positive → fixes
#190✓.- Legacy installs (no
source_repo):installedKeysentry ="weather"→ the fallbackinstalledKeys.has(skill.name.toLowerCase())marks all registry skills with that name as installed, which is the original bug behaviour.This is intentional per the PR description, but it is worth a brief inline comment so future contributors understand why the fallback exists and the trade-off it carries:
📝 Suggested documentation comment
+ // installedKeys uses source_repo/name when available (new installs) for + // per-repo precision. Skills installed before this change have no source_repo + // and fall back to name-only matching, which re-exhibits the original false- + // positive behaviour until those skills are re-installed. const installedKeys = new Set( installedSkills.map((s) => s.source_repo ? `${s.source_repo}/${s.name}`.toLowerCase() : s.name.toLowerCase(), ), );Also applies to: 371-374
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@interface/src/routes/AgentSkills.tsx` around lines 234 - 240, Add a brief inline comment next to the installedKeys construction and the fallback membership check explaining that legacy installs (installedSkills entries without source_repo) are stored as name-only (e.g., "weather"), and the fallback installedKeys.has(skill.name.toLowerCase()) will therefore mark any registry skill with the same name as installed; state this is an intentional trade-off to preserve legacy behavior and reference the compositeKey (`${skill.source}/${skill.name}`) versus name-only key formats so future contributors understand the limitation and why the fallback exists.
506-517:key={skill.name}onInstalledSkillis safe today but fragile.
SkillSetalready enforces one-skill-per-lowercase-name, so duplicate keys cannot occur now. However, if that invariant ever relaxes (e.g. a future multi-source install mode), React will silently render only the last matching child.Consider
key={skill.source_repo ? \${skill.source_repo}/${skill.name}` : skill.name}` to make the key stable and unique under any future extension.✏️ Proposed change
- key={skill.name} + key={skill.source_repo ? `${skill.source_repo}/${skill.name}` : skill.name}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@interface/src/routes/AgentSkills.tsx` around lines 506 - 517, The current InstalledSkill list uses key={skill.name} which relies on a fragile uniqueness invariant; change the key generation in the installedSkills.map to a stable unique composite (e.g., include skill.source_repo when present) so keys remain unique if multiple skills share a name. Update the InstalledSkill mapping to compute the key as something like `${skill.source_repo}/${skill.name}` when skill.source_repo exists, otherwise fallback to skill.name, ensuring InstalledSkill receives that composite key and preventing React child key collisions in future multi-source installs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/skills/installer.rs`:
- Around line 184-198: The read of SKILL.md currently swallows errors with `if
let Ok(content) = fs::read_to_string(&skill_md).await`, so add error
handling/logging for the read path: replace the `if let Ok(...)` with a match or
`if let Err(e)` branch so that when `fs::read_to_string(&skill_md).await` fails
you emit a `tracing::warn!` (including `skill = %skill_name`, `%e`, and a clear
message like "failed to read SKILL.md") before returning/continuing; keep the
existing logic that calls `inject_source_repo(&content, repo)` and logs write
failures for `fs::write(&skill_md, patched).await`.
---
Outside diff comments:
In `@src/skills/installer.rs`:
- Around line 36-41: The download currently uses reqwest::Client::new() with no
timeout which can hang; replace the client creation with a timed client (use
reqwest::Client::builder().timeout(Duration::from_secs(10)).build()) and use
that client for the existing client.get(&download_url).send().await call, adding
a std::time::Duration import if missing so the GitHub archive download in
installer.rs times out like the registry proxy does.
---
Nitpick comments:
In `@interface/src/routes/AgentSkills.tsx`:
- Around line 234-240: Add a brief inline comment next to the installedKeys
construction and the fallback membership check explaining that legacy installs
(installedSkills entries without source_repo) are stored as name-only (e.g.,
"weather"), and the fallback installedKeys.has(skill.name.toLowerCase()) will
therefore mark any registry skill with the same name as installed; state this is
an intentional trade-off to preserve legacy behavior and reference the
compositeKey (`${skill.source}/${skill.name}`) versus name-only key formats so
future contributors understand the limitation and why the fallback exists.
- Around line 506-517: The current InstalledSkill list uses key={skill.name}
which relies on a fragile uniqueness invariant; change the key generation in the
installedSkills.map to a stable unique composite (e.g., include
skill.source_repo when present) so keys remain unique if multiple skills share a
name. Update the InstalledSkill mapping to compute the key as something like
`${skill.source_repo}/${skill.name}` when skill.source_repo exists, otherwise
fallback to skill.name, ensuring InstalledSkill receives that composite key and
preventing React child key collisions in future multi-source installs.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
interface/src/api/client.tsinterface/src/routes/AgentSkills.tsxsrc/api/skills.rssrc/skills.rssrc/skills/installer.rs
edc37b4 to
268c1a2
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/skills/installer.rs`:
- Around line 232-259: The current final assembly uses
format!("---{new_fm}\n---{body}") which concatenates the closing frontmatter
delimiter and the body without ensuring a separating newline; update the
assembly to always emit the delimiter followed by a newline before body (e.g.,
format!("---\n{new_fm}\n---\n{body}")), ensuring newlines around the
opening/closing '---' and referencing the same new_fm and body variables so
frontmatter parsing isn't broken.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
interface/src/api/client.tsinterface/src/routes/AgentSkills.tsxsrc/api/skills.rssrc/skills.rssrc/skills/installer.rs
🚧 Files skipped from review as they are similar to previous changes (4)
- src/api/skills.rs
- interface/src/routes/AgentSkills.tsx
- interface/src/api/client.ts
- src/skills.rs
268c1a2 to
6e40aba
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/skills/installer.rs (2)
382-438: Good test coverage forinject_source_repo— edge cases and roundtrip withparse_frontmatterare well covered.One gap worth noting: there's no test for content with leading whitespace before the frontmatter delimiter (e.g.
" ---\nname: x\n---\n").trim_start()on line 224 handles this, but a test would document the behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/skills/installer.rs` around lines 382 - 438, Add a new unit test that verifies inject_source_repo correctly handles leading whitespace before the frontmatter delimiter: create content with leading spaces before the initial "---" (e.g. " ---\nname: x\n---\n\n# Body\n"), call inject_source_repo(content, "owner/repo"), and assert the returned string contains a single source_repo: owner/repo inside the frontmatter (use parse_frontmatter or string checks), preserves the existing fields (like name: x) and retains the body "# Body". Reference inject_source_repo and parse_frontmatter to locate where the test should sit alongside the other inject_source_repo tests.
222-259:inject_source_repo: minor leading-blank-line artifact in rewritten frontmatter.When
fm_blockstarts with\n(which it always does, sinceafter_openingbegins right after the opening---), the first element from.lines()is"". Afterjoin("\n"),new_fmstarts with an empty line, so the output looks like:--- ← blank line here name: weather source_repo: anthropics/skills ---This is functionally harmless —
parse_frontmattertrims and skips blank lines — but it's a minor aesthetic wart. Consider stripping the leading newline fromfm_blockbefore splitting:♻️ Optional cleanup
- let fm_block = &after_opening[..end_pos]; + let fm_block = after_opening[..end_pos].trim_start_matches('\n');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/skills/installer.rs` around lines 222 - 259, The inject_source_repo function produces a leading blank line in the rewritten frontmatter because fm_block begins with a newline; fix it by normalizing fm_block before splitting (e.g., remove a leading '\n' or use trim_start_matches on fm_block) so that .lines() doesn't yield an empty first element, then proceed with the existing filtering/joining logic to append the new source_repo line.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/skills/installer.rs`:
- Around line 382-438: Add a new unit test that verifies inject_source_repo
correctly handles leading whitespace before the frontmatter delimiter: create
content with leading spaces before the initial "---" (e.g. " ---\nname:
x\n---\n\n# Body\n"), call inject_source_repo(content, "owner/repo"), and assert
the returned string contains a single source_repo: owner/repo inside the
frontmatter (use parse_frontmatter or string checks), preserves the existing
fields (like name: x) and retains the body "# Body". Reference
inject_source_repo and parse_frontmatter to locate where the test should sit
alongside the other inject_source_repo tests.
- Around line 222-259: The inject_source_repo function produces a leading blank
line in the rewritten frontmatter because fm_block begins with a newline; fix it
by normalizing fm_block before splitting (e.g., remove a leading '\n' or use
trim_start_matches on fm_block) so that .lines() doesn't yield an empty first
element, then proceed with the existing filtering/joining logic to append the
new source_repo line.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
interface/src/api/client.tsinterface/src/routes/AgentSkills.tsxsrc/api/skills.rssrc/skills.rssrc/skills/installer.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- src/api/skills.rs
- interface/src/api/client.ts
- src/skills.rs
Summary
source_repo(e.g.anthropics/skills) in SKILL.md frontmatter during GitHub installsource_repothrough the API and usessource_repo/nameas a composite key for installed-status checks in the frontendTest plan
cargo test --lib skills— all 14 tests pass (including 5 new tests forinject_source_repo)source_repoin frontmatterNote
Changes overview: Adds
source_repotracking throughout the skill system. Backend now captures GitHub org/repo during installation viainject_source_repo()helper that safely modifies SKILL.md frontmatter (handles missing/malformed frontmatter gracefully). Frontend UI updated to build composite keys (source_repo/name) for installed-skill lookups, with fallback to name-only matching for backward compatibility. API surfaces the new optionalsource_repofield. All types (Skill, SkillInfo, SkillSet) updated to carry this metadata.Written by Tembo for commit 826f255. This will update automatically on new commits.